package org.xx.armory.rpc;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.xx.armory.commons.StringEnum;
import org.xx.armory.commons.ValueEnum;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

import static java.util.Base64.getDecoder;
import static org.apache.commons.lang3.StringUtils.split;
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
import static org.apache.commons.lang3.math.NumberUtils.toByte;
import static org.apache.commons.lang3.math.NumberUtils.toDouble;
import static org.apache.commons.lang3.math.NumberUtils.toFloat;
import static org.apache.commons.lang3.math.NumberUtils.toInt;
import static org.apache.commons.lang3.math.NumberUtils.toLong;
import static org.apache.commons.lang3.math.NumberUtils.toShort;
import static org.xx.armory.commons.Converter.toStringEnum;
import static org.xx.armory.commons.Converter.toValueEnum;
import static org.xx.armory.commons.SysUtils.removeNonDigit;
import static org.xx.armory.commons.Validators.notNull;

/**
 * 参数转换工具类。
 */
public final class ParamConverters {
    private static final char ARRAY_SEPARATOR = ',';
    private static final Map<Class<?>, ParamConverter> CONVERTER_MAP = initializeConverters();

    private ParamConverters() {
    }

    private static Map<Class<?>, ParamConverter> initializeConverters() {
        final Map<Class<?>, ParamConverter> result = new ConcurrentHashMap<>();

        result.put(String.class, ParamConverters::toString);
        result.put(int.class, ParamConverters::toIntValue);
        result.put(long.class, ParamConverters::toLongValue);
        result.put(byte.class, ParamConverters::toByteValue);
        result.put(short.class, ParamConverters::toShortValue);
        result.put(float.class, ParamConverters::toFloatValue);
        result.put(double.class, ParamConverters::toDoubleValue);
        result.put(boolean.class, ParamConverters::toBooleanValue);
        result.put(char.class, ParamConverters::toCharValue);
        result.put(Date.class, ParamConverters::toDate);
        result.put(BigInteger.class, ParamConverters::toBigInteger);
        result.put(BigDecimal.class, ParamConverters::toBigDecimal);
        result.put(Integer.class, ParamConverters::toIntObject);
        result.put(Long.class, ParamConverters::toLongObject);
        result.put(Byte.class, ParamConverters::toByteObject);
        result.put(Short.class, ParamConverters::toShortObject);
        result.put(Float.class, ParamConverters::toFloatObject);
        result.put(Double.class, ParamConverters::toDoubleObject);
        result.put(Boolean.class, ParamConverters::toBooleanObject);
        result.put(Character.class, ParamConverters::toCharObject);
        result.put(int[].class, ParamConverters::toIntArray);
        result.put(long[].class, ParamConverters::toLongArray);
        result.put(byte[].class, ParamConverters::toByteArray);
        result.put(double[].class, ParamConverters::toDoubleArray);
        result.put(char[].class, ParamConverters::toCharArray);
        result.put(BigInteger[].class, ParamConverters::toBigIntegerArray);
        result.put(BigDecimal[].class, ParamConverters::toBigDecimalArray);
        result.put(Date[].class, ParamConverters::toDateArray);
        result.put(Boolean[].class, ParamConverters::toBooleanArray);
        result.put(String[].class, ParamConverters::toStringArray);

        final StringBuilder convertersLog = new StringBuilder();
        convertersLog.append("---------------------\n");
        for (final Map.Entry<Class<?>, ParamConverter> entry : result.entrySet()) {
            convertersLog.append(entry.getKey().getSimpleName()).append(": ").append(entry.getValue()).append("\n");
        }
        convertersLog.append("---------------------\n");

        LoggerFactory.getLogger(ParamConverters.class).debug("registered parameter converters: \n{}", convertersLog);

        return result;
    }

    /**
     * 为指定的类型注册一个转换器。
     *
     * @param type
     *         转换器针对的类型。
     * @param converter
     *         转换器。
     * @return 此类型原先使用的转换器，如果不存在则返回{@code null}。
     * @throws IllegalArgumentException
     *         如果参数{@code type}或者{@code converter}是{@code null}。
     */
    public static ParamConverter register(
            Class<?> type,
            ParamConverter converter
    ) {
        notNull(type, "type");
        notNull(converter, "converter");

        return CONVERTER_MAP.put(type, converter);
    }

    /**
     * 将字符串转换为指定的类型。
     *
     * @param value
     *         待转化的字符串。
     * @param type
     *         目标类型。
     * @return 转化后的结果，此结果的类型是指定的目标类型，如果参数{@code value}是{@code null}则返回{@code null}。
     */
    public static Object convert(
            String value,
            Class<?> type
    ) {
        if (value == null) {
            return null;
        }

        if (type.isEnum()) {
            // 枚举类型需要特殊处理。
            if (ValueEnum.class.isAssignableFrom(type)) {
                return toValueEnumObject(value, type.asSubclass(ValueEnum.class));
            } else if (StringEnum.class.isAssignableFrom(type)) {
                return toStringEnumObject(value, type.asSubclass(StringEnum.class));
            } else {
                throw new IllegalArgumentException("unknown enum: " + type);
            }
        }

        final ParamConverter converter = CONVERTER_MAP.getOrDefault(type, null);
        if (converter == null) {
            throw new IllegalArgumentException("unknown type: " + type);
        }

        return converter.apply(value);
    }

    private static String toString(
            String value
    ) {
        return value;
    }

    private static int toIntValue(
            String value
    ) {
        return toInt(removeNonDigit(value));
    }


    private static long toLongValue(
            String value
    ) {
        return toLong(removeNonDigit(value));
    }

    private static byte toByteValue(
            String value
    ) {
        return toByte(removeNonDigit(value));
    }

    private static short toShortValue(
            String value
    ) {
        return toShort(removeNonDigit(value));
    }

    private static float toFloatValue(
            String value
    ) {
        return toFloat(removeNonDigit(value));
    }

    private static double toDoubleValue(
            String value
    ) {
        return toDouble(removeNonDigit(value));
    }

    private static char toCharValue(
            String value
    ) {
        return value.isEmpty() ? ' ' : value.charAt(0);
    }

    private static Boolean toBoolean(
            String value
    ) {
        value = trimToEmpty(value).toLowerCase();
        if ("t".equals(value) || "true".equals(value) || "y".equals(value) || "yes".equals(value) || "on".equals(value) || "1".equals(value)) {
            return Boolean.TRUE;
        } else if ("f".equals(value) || "false".equals(value) || "n".equals(value) || "no".equals(value) || "off".equals(value) || "0".equals(value)) {
            return Boolean.FALSE;
        } else {
            return null;
        }
    }

    private static boolean toBooleanValue(
            String value
    ) {
        final Boolean r = toBoolean(value);
        return r != null ? r : false;
    }

    private static Integer toIntObject(
            String value
    ) {
        value = removeNonDigit(value);
        return value.isEmpty() ? null : toInt(value);
    }


    private static Long toLongObject(
            String value
    ) {
        value = removeNonDigit(value);
        return value.isEmpty() ? null : toLong(value);
    }

    private static Byte toByteObject(
            String value
    ) {
        value = removeNonDigit(value);
        return value.isEmpty() ? null : toByte(value);
    }

    private static Short toShortObject(
            String value
    ) {
        value = removeNonDigit(value);
        return value.isEmpty() ? null : toShort(value);
    }

    private static Float toFloatObject(
            String value
    ) {
        value = removeNonDigit(value);
        return value.isEmpty() ? null : toFloat(value);
    }

    private static Double toDoubleObject(
            String value
    ) {
        value = removeNonDigit(value);
        return value.isEmpty() ? null : toDouble(value);
    }

    private static Character toCharObject(
            String value
    ) {
        return value.isEmpty() ? null : value.charAt(0);
    }

    private static Boolean toBooleanObject(
            String value
    ) {
        return toBoolean(value);
    }

    private static BigInteger toBigInteger(
            String value
    ) {
        try {
            return value.isEmpty() ? null : new BigInteger(removeNonDigit(value));
        } catch (NumberFormatException ex) {
            return null;
        }
    }

    private static BigDecimal toBigDecimal(
            String value
    ) {
        try {
            return value.isEmpty() ? null : new BigDecimal(removeNonDigit(value));
        } catch (NumberFormatException ex) {
            return null;
        }
    }

    private static Date toDate(
            String value
    ) {
        return value.isEmpty() ? null : new Date(toLong(removeNonDigit(value)));
    }

    private static Object toValueEnumObject(
            String value,
            Class<? extends ValueEnum> type
    ) {
        return value.isEmpty() ? null : toValueEnum(type.getEnumConstants(), toLongValue(value));
    }

    private static Object toStringEnumObject(
            String value,
            Class<? extends StringEnum> type
    ) {
        return value.isEmpty() ? null : toStringEnum(type.getEnumConstants(), value);
    }

    private static String[] toStringArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new String[0];
        }

        return split(value, ARRAY_SEPARATOR);
    }

    private static byte[] toByteArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new byte[0];
        }

        try {
            return getDecoder().decode(value);
        } catch (IllegalArgumentException ex) {
            return new byte[0];
        }
    }

    private static int[] toIntArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new int[0];
        }

        return Stream.of(split(value, ARRAY_SEPARATOR))
                     .filter(StringUtils::isNotBlank)
                     .mapToInt(ParamConverters::toIntValue)
                     .toArray();
    }

    private static long[] toLongArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new long[0];
        }

        return Stream.of(split(value, ARRAY_SEPARATOR))
                     .filter(StringUtils::isNotBlank)
                     .mapToLong(ParamConverters::toLongValue)
                     .toArray();
    }

    private static double[] toDoubleArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new double[0];
        }

        return Stream.of(split(value, ARRAY_SEPARATOR))
                     .filter(StringUtils::isNotBlank)
                     .mapToDouble(ParamConverters::toDoubleValue)
                     .toArray();
    }

    private static char[] toCharArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new char[0];
        }

        final String[] array = Stream.of(split(value, ARRAY_SEPARATOR))
                                     .filter(StringUtils::isNotBlank)
                                     .toArray(String[]::new);
        final char[] result = new char[array.length];
        for (int i = 0; i < array.length; ++i) {
            result[i] = toCharValue(array[i]);
        }

        return result;
    }

    private static boolean[] toBooleanArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new boolean[0];
        }

        final String[] array = Stream.of(split(value, ARRAY_SEPARATOR))
                                     .filter(StringUtils::isNotBlank)
                                     .toArray(String[]::new);
        final boolean[] result = new boolean[array.length];
        for (int i = 0; i < array.length; ++i) {
            result[i] = toBooleanValue(array[i]);
        }

        return result;
    }

    private static BigInteger[] toBigIntegerArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new BigInteger[0];
        }

        return Stream.of(split(value, ARRAY_SEPARATOR))
                     .filter(StringUtils::isNotBlank)
                     .map(ParamConverters::toBigInteger)
                     .filter(Objects::nonNull)
                     .toArray(BigInteger[]::new);
    }

    private static BigDecimal[] toBigDecimalArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new BigDecimal[0];
        }

        return Stream.of(split(value, ARRAY_SEPARATOR))
                     .filter(StringUtils::isNotBlank)
                     .map(ParamConverters::toBigInteger)
                     .filter(Objects::nonNull)
                     .toArray(BigDecimal[]::new);
    }

    private static Date[] toDateArray(
            String value
    ) {
        if (value.isEmpty()) {
            return new Date[0];
        }

        return Stream.of(split(value, ARRAY_SEPARATOR))
                     .filter(StringUtils::isNotBlank)
                     .map(ParamConverters::toDate)
                     .filter(Objects::nonNull)
                     .toArray(Date[]::new);
    }

}
